# Carga de librerías
if (!require(summarytools)){
  install.packages('summarytools', repos='http://cran.us.r-project.org')
  library(summarytools)
} 
## Loading required package: summarytools
if(!require(tidyverse)){
  install.packages('tidyverse', repos='http://cran.us.r-project.org')
  library(tidyverse)
}
## Loading required package: tidyverse
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ──
## ✓ ggplot2 3.3.5     ✓ purrr   0.3.4
## ✓ tibble  3.1.5     ✓ dplyr   1.0.7
## ✓ tidyr   1.1.4     ✓ stringr 1.4.0
## ✓ readr   2.0.2     ✓ forcats 0.5.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
## x tibble::view()  masks summarytools::view()
if(!require(caret)){
  install.packages('caret', repos='http://cran.us.r-project.org')
  library(caret)
}
## Loading required package: caret
## Loading required package: lattice
## 
## Attaching package: 'caret'
## The following object is masked from 'package:purrr':
## 
##     lift
if(!require(corrplot)){
  install.packages('corrplot', repos='http://cran.us.r-project.org')
  library(corrplot)
}
## Loading required package: corrplot
## corrplot 0.92 loaded
if(!require(wordcloud)){
  install.packages('wordcloud', repos='http://cran.us.r-project.org')
  library(wordcloud)
}
## Loading required package: wordcloud
## Loading required package: RColorBrewer
if(!require(Rcpp)){
  install.packages('Rcpp', repos='http://cran.us.r-project.org')
  library(Rcpp)
}
## Loading required package: Rcpp
if(!require(xlsx)){
  install.packages('xlsx', repos='http://cran.us.r-project.org')
  library(xlsx)
}
## Loading required package: xlsx
if(!require(plyr)){
  install.packages('plyr', repos='http://cran.us.r-project.org')
  library(plyr)
}
## Loading required package: plyr
## ------------------------------------------------------------------------------
## You have loaded plyr after dplyr - this is likely to cause problems.
## If you need functions from both plyr and dplyr, please load plyr first, then dplyr:
## library(plyr); library(dplyr)
## ------------------------------------------------------------------------------
## 
## Attaching package: 'plyr'
## The following objects are masked from 'package:dplyr':
## 
##     arrange, count, desc, failwith, id, mutate, rename, summarise,
##     summarize
## The following object is masked from 'package:purrr':
## 
##     compact
if(!require(BSDA)){
  install.packages('BSDA', repos='http://cran.us.r-project.org')
  library(BSDA)
}
## Loading required package: BSDA
## 
## Attaching package: 'BSDA'
## The following object is masked from 'package:datasets':
## 
##     Orange
if(!require(leaflet)){
  install.packages('leaflet', repos='http://cran.us.r-project.org')
  library(leaflet)
}
## Loading required package: leaflet
if(!require(tm)){
  install.packages('tm', repos='http://cran.us.r-project.org')
  library(tm)}
## Loading required package: tm
## Loading required package: NLP
## 
## Attaching package: 'NLP'
## The following object is masked from 'package:ggplot2':
## 
##     annotate

1 Descripción del dataset


Madrid se ha convertido en uno de los destinos predilectos de la inversión inmobiliaria mexicana. El idioma, los lazos culturales entre ambos países, la oferta gastronómica, así como la seguridad jurídica y ciudadana, entre otros atractivos, han provocado el interés del capital inversor mexicano en la capital de España.

Una conocida firma de inversión inmobiliaria mexicana ha pedido realizar un estudio sobre la situación del mercado inmobiliario en la ciudad de Madrid. El problema a resolver es sencillo: ¿Dónde invertir?

El inversor quiere respuesta a estas preguntas:

Es un hecho conocido que el mercado de alquiler se encuentra en franco retroceso debido al auge de los alquileres turísticos. Los propietarios han cambiado el alquiler tradicional por el turístico, espoleado por compañías de impacto mundial como AirBnB. El efecto sobre el alquiler tradicional ha sido nefasto. Por un lado, la oferta de alquileres se ha reducido, lo que ha tenido un gran impacto en los precios, y por otro lado ha generado un flujo de habitantes hacia las zonas periféricas de la capital. El centro de la ciudad es de los turistas.

Debido a esto, y para dar respuesta a nuestro inversor mexicano, se utilizan datos de AirBnB en Madrid.

De todos los datos disponibles en este repositorio se utilizan los siguientes datasets:

También se utilizará el siguiente dataset:

El mejor enfoque para poder proporcionar una respuesta adecuada a un problema complejo, es dividir el problema en partes más pequeñas, más sencillas de responder, es por ello que se definen una serie de preguntas más específicas. Estas preguntas se hacen desde dos ángulos distintos: por un lado queremos analizar el aspecto geográfico, ¿dónde invertir?, y por otro lado lo concerniente al tipo de alojamiento a comprar ¿Qué comprar y para qué?.

Un poco de geografía: La ciudad de Madrid se divide en 21 distritos. La presencia de la carretera de circunvalación M-30 actúa como barrera geográfica entre los distritos de la almendra central (interior) de los del extrarradio. Dentro de la almendra central se encuentran los siguientes distritos (entre corchetes el precio de alquiler tradicional según el portal [Idealista] a Noviembre de 2020 (https://www.idealista.com/sala-de-prensa/informes-precio-vivienda/alquiler/madrid-comunidad/madrid-provincia/madrid/)): Centro [17,5 euros/m2], Tetuán [15,0 euros/m2], Chamartín [15,4 euros/m2], Chamberí [17,1 euros/m2], Salamanca [17,5 euros/m2], Retiro [15,0 euros/m2] y Arganzuela [14,6 euros/m2].

En el extrarradio tenemos los siguientes distritos: Barajas [11,4 euros/m2], Carabanchel [11,7 euros/m2], Ciudad Lineal [12,8 euros/m2], Fuencarral [15,4 euros/m2],Hortaleza [12,5 euros/m2], Latina [11,8 euros/m2], Moncloa [14,3 euros/m2], Moratalaz [10,9 euros/m2], Puente de Vallecas [12,1 euros/m2], San Blas [11,4 euros/m2], Usera [11,5 euros/m2], Vicálvaro [10,4 euros/m2], Villa de Vallecas [11,2 euros/m2], Villaverde [10,9 euros/m2].

Estas son las preguntas relativas a la geografía de Madrid:

Airbnb oferta 4 tipos de alojamientos: alojamiento completo, habitación privada, habitación compartida y habitación de hotel.Las preguntas a responder en este grupo son:

Para responder a todas estas preguntas se usan técnicas de ciencia de datos. Todo comienza con la carga de los mismos.

1.1 Carga de los datasets

1.1.1 listings.csv

# Limpiamos el workspace
rm(list = ls())

# Cargamos datasets 
df_listings <- read.csv('listings.csv', encoding = "UTF-8")

df_reviews <- read.csv('reviews_detailed.csv', header = TRUE)

df_population <- read.xlsx('C5000121.xls',sheetIndex=1, startRow=11, endRow=31, as.data.frame=TRUE, header=FALSE, colIndex=c(2,3))
colnames(df_population) <- c('neighbourhood_group', 'population')

Se carga el dataset y se visualiza su estructura.

print(dfSummary(df_listings), method = 'render')

Data Frame Summary

df_listings

Dimensions: 19618 x 16
Duplicates: 0
No Variable Stats / Values Freqs (% of Valid) Graph Valid Missing
1 id [integer]
Mean (sd) : 29122004 (13518392)
min ≤ med ≤ max:
6369 ≤ 31875060 ≤ 49187791
IQR (CV) : 21875702 (0.5)
19618 distinct values 19618 (100.0%) 0 (0.0%)
2 name [character]
1. Habitación privada
2. HABITACIÓN COMPARTIDA/SHA
3. HABITACIÓN FEMENINA COMPA
4. 2Bedroom 2Bathroom aparta
5. APARTMENT 4PAX GRAN VIA C
6. Madrid Center: Puerta del
7. Nuevo Apartamento junto a
8. 2 Bedroom 1 Bathroom apar
9. Habitación en Madrid
10. Private bedroom in cute M
[ 18784 others ]
22(0.1%)
20(0.1%)
15(0.1%)
13(0.1%)
12(0.1%)
12(0.1%)
12(0.1%)
10(0.1%)
10(0.1%)
10(0.1%)
19482(99.3%)
19618 (100.0%) 0 (0.0%)
3 host_id [integer]
Mean (sd) : 131216503 (116678974)
min ≤ med ≤ max:
7952 ≤ 99018982 ≤ 396428081
IQR (CV) : 198036634 (0.9)
11325 distinct values 19618 (100.0%) 0 (0.0%)
4 host_name [character]
1. (Empty string)
2. Daniel
3. Carlos
4. Javier
5. Maria
6. Ana
7. Marco
8. Luis
9. Miguel
10. Jorge
[ 3891 others ]
527(2.7%)
255(1.3%)
231(1.2%)
230(1.2%)
192(1.0%)
186(0.9%)
177(0.9%)
163(0.8%)
161(0.8%)
151(0.8%)
17345(88.4%)
19618 (100.0%) 0 (0.0%)
5 neighbourhood_group [character]
1. Centro
2. Salamanca
3. Chamberí
4. Arganzuela
5. Tetuán
6. Carabanchel
7. Retiro
8. Ciudad Lineal
9. Puente de Vallecas
10. Latina
[ 11 others ]
8649(44.1%)
1324(6.7%)
1252(6.4%)
1104(5.6%)
816(4.2%)
708(3.6%)
664(3.4%)
649(3.3%)
617(3.1%)
608(3.1%)
3227(16.4%)
19618 (100.0%) 0 (0.0%)
6 neighbourhood [character]
1. Embajadores
2. Universidad
3. Palacio
4. Sol
5. Justicia
6. Cortes
7. Trafalgar
8. Palos de Moguer
9. Goya
10. Argüelles
[ 118 others ]
2318(11.8%)
1869(9.5%)
1506(7.7%)
1125(5.7%)
951(4.8%)
880(4.5%)
371(1.9%)
337(1.7%)
296(1.5%)
281(1.4%)
9684(49.4%)
19618 (100.0%) 0 (0.0%)
7 latitude [numeric]
Mean (sd) : 40.4 (0)
min ≤ med ≤ max:
40.3 ≤ 40.4 ≤ 40.6
IQR (CV) : 0 (0)
7536 distinct values 19618 (100.0%) 0 (0.0%)
8 longitude [numeric]
Mean (sd) : -3.7 (0)
min ≤ med ≤ max:
-3.9 ≤ -3.7 ≤ -3.5
IQR (CV) : 0 (0)
7595 distinct values 19618 (100.0%) 0 (0.0%)
9 room_type [character]
1. Entire home/apt
2. Hotel room
3. Private room
4. Shared room
11314(57.7%)
166(0.8%)
7809(39.8%)
329(1.7%)
19618 (100.0%) 0 (0.0%)
10 price [integer]
Mean (sd) : 129.3 (484.1)
min ≤ med ≤ max:
0 ≤ 58 ≤ 9999
IQR (CV) : 65 (3.7)
544 distinct values 19618 (100.0%) 0 (0.0%)
11 minimum_nights [integer]
Mean (sd) : 6.6 (33.3)
min ≤ med ≤ max:
1 ≤ 2 ≤ 1125
IQR (CV) : 2 (5.1)
81 distinct values 19618 (100.0%) 0 (0.0%)
12 number_of_reviews [integer]
Mean (sd) : 31.9 (63.9)
min ≤ med ≤ max:
0 ≤ 4 ≤ 706
IQR (CV) : 31 (2)
435 distinct values 19618 (100.0%) 0 (0.0%)
13 last_review [character]
1. (Empty string)
2. 2020-03-01
3. 2020-03-08
4. 2021-03-31
5. 2020-03-02
6. 2021-04-11
7. 2021-04-05
8. 2021-04-04
9. 2020-03-09
10. 2021-03-28
[ 1581 others ]
5637(28.7%)
359(1.8%)
314(1.6%)
201(1.0%)
195(1.0%)
192(1.0%)
160(0.8%)
158(0.8%)
150(0.8%)
150(0.8%)
12102(61.7%)
19618 (100.0%) 0 (0.0%)
14 reviews_per_month [numeric]
Mean (sd) : 1.1 (1.3)
min ≤ med ≤ max:
0 ≤ 0.6 ≤ 16.2
IQR (CV) : 1.5 (1.2)
682 distinct values 13981 (71.3%) 5637 (28.7%)
15 calculated_host_listings_count [integer]
Mean (sd) : 10.2 (23.5)
min ≤ med ≤ max:
1 ≤ 2 ≤ 163
IQR (CV) : 5 (2.3)
51 distinct values 19618 (100.0%) 0 (0.0%)
16 availability_365 [integer]
Mean (sd) : 159.1 (144.3)
min ≤ med ≤ max:
0 ≤ 126 ≤ 365
IQR (CV) : 320 (0.9)
366 distinct values 19618 (100.0%) 0 (0.0%)

Generated by summarytools 1.0.0 (R version 4.1.1)
2021-12-27

El Data Frame Summary muestra la siguiente información:

  • Dimensiones: 19618 registros x 16 variables.
  • Duplicados: 0.
  • Valores nulos (columna Missing): para la variable 14, reviews_per_month [numeric], hay 5637 valores perdidos, que representan el 28.7%.

También se muestra las estadísticas de cada variable, las frecuencias de los valores de las variables, gráficos con la distribución de frecuencias, y número y porcentaje de valores válidos de cada variable. Salvo para la variable reviews_per_month, los valores válidos son el 100% para el resto de las variables.

Tenemos 6 variables discretas de tipo character:

  • name: descripción del alojamiento.

  • host_name: nombre del anfitrión (por lo general, sólo el nombre).

  • neighbourhood_group: distrito. Toma 21 valores diferentes. Se observa que el 44.1% de los alojamientos están en el distrito Centro.

  • neighbourhood: barrio. Toma 128 valores diferentes.

  • room_type: tipo de alojamiento Toma 4 valores diferentes. Se observa que el 57.7% es para apartamento completo (Entire home/apt) y el 39.8% para habitación privada (Private room).

  • last_review: fecha de la ultima reseña. La cadena vacía representa que no hay reseña.

Tenemos 10 variables numéricas:

  • id: identificador del anuncio.

  • host_id: identificador del anfitrión.

  • latitude: latitud según el Sistema Geodésico Mundial (WGS84).

  • longitude: longitud según el Sistema Geodésico Mundial (WGS84).

  • price: precio diario en moneda local.

Se observa que el precio mínimo es 0, y el máximo 9999, lo que podría estar indicando que la ausencia de valores se ha codificado como 0 ó 9999, dependiendo del caso. Estos valores aparecerán como outliers (valores extremos).

  • minimum_nights: cantidad mínima de noches de estancia para el alojamiento (las reglas del calendario pueden ser diferentes). El valor máximo toma un valor excesivamente alto (1125), lo que podría estar indicando una captura errónea del dato. Estos valores aparecerán como outliers.

  • number_of_reviews: número de reseñas del anuncio.

  • reviews_per_month: número de reseñas que tiene el anuncio durante su vida útil

  • calculated_host_listings_count: Número de casas / apartamentos completos que tiene el anfitrión en el scrape actual, en la geografía de la ciudad / región. Observamos que el anfitrión con más alojamientos tiene 163.

  • availability_365: Disponibilidad 365 días según determine el calendario. Hay que tener en cuenta que un anuncio puede no estar disponible porque ha sido reservado por un invitado o bloqueado por el anfitrión.

Es el principal dataset que se va a emplear en este análisis.

1.1.2 reviews_detailed.csv

print(dfSummary(df_reviews), method = 'render')

Data Frame Summary

df_reviews

Dimensions: 625006 x 6
Duplicates: 0
No Variable Stats / Values Freqs (% of Valid) Graph Valid Missing
1 listing_id [integer]
Mean (sd) : 16640054 (11030402)
min ≤ med ≤ max:
6369 ≤ 16446376 ≤ 49067339
IQR (CV) : 16287097 (0.7)
13981 distinct values 625006 (100.0%) 0 (0.0%)
2 id [integer]
Mean (sd) : 354375214 (196749914)
min ≤ med ≤ max:
29428 ≤ 347415986 ≤ 749371843
IQR (CV) : 335785624 (0.6)
625006 distinct values 625006 (100.0%) 0 (0.0%)
3 date [character]
1. 2019-11-03
2. 2019-12-08
3. 2019-06-02
4. 2018-12-09
5. 2020-03-01
6. 2019-11-24
7. 2019-12-01
8. 2019-10-06
9. 2019-10-13
10. 2020-01-26
[ 3576 others ]
1514(0.2%)
1446(0.2%)
1435(0.2%)
1372(0.2%)
1371(0.2%)
1363(0.2%)
1294(0.2%)
1254(0.2%)
1253(0.2%)
1240(0.2%)
611464(97.8%)
625006 (100.0%) 0 (0.0%)
4 reviewer_id [integer]
Mean (sd) : 100167823 (88847822)
min ≤ med ≤ max:
125 ≤ 72466528 ≤ 396608119
IQR (CV) : 127857547 (0.9)
564931 distinct values 625006 (100.0%) 0 (0.0%)
5 reviewer_name [character]
1. David
2. Laura
3. Maria
4. Carlos
5. Daniel
6. Ana
7. Javier
8. Andrea
9. Juan
10. Jorge
[ 75055 others ]
4924(0.8%)
4433(0.7%)
4037(0.6%)
3968(0.6%)
3923(0.6%)
3405(0.5%)
3019(0.5%)
2709(0.4%)
2561(0.4%)
2501(0.4%)
589526(94.3%)
625006 (100.0%) 0 (0.0%)
6 comments [character]
1. .
2. Todo perfecto
3. Excelente
4. (Empty string)
5. The host canceled this re
6. The host canceled this re
7. Muy bien
8. Todo muy bien
9. Muy recomendable
10. Great location
[ 599607 others ]
1085(0.2%)
540(0.1%)
451(0.1%)
329(0.1%)
315(0.1%)
311(0.0%)
287(0.0%)
274(0.0%)
271(0.0%)
268(0.0%)
620871(99.3%)
625002 (100.0%) 4 (0.0%)

Generated by summarytools 1.0.0 (R version 4.1.1)
2021-12-27

  • Dimensiones: 625006 registros x 6 variables.
  • Duplicados: 0.
  • Valores nulos (columna Missing): para la variable 6, comments [character], hay 4 valores perdidos.

Hay 3 variables discretas de tipo character:

  • date: fecha de la reseña.

  • reviewer_name: nombre del cliente que deja la reseña.

  • comments : comentarios del cliente.

Y 3 variables numéricas:

  • listing_id : identificador del anuncio.

  • id: identificador de la reseña.

  • reviewer_id: identificador del cliente que deja la reseña.

Este dataset se utilizará para analizar la evolución temporal del alquiler turístico.

1.1.3 C5000121.xls

print(dfSummary(df_population), method = 'render')

Data Frame Summary

df_population

Dimensions: 21 x 2
Duplicates: 0
No Variable Stats / Values Freqs (% of Valid) Graph Valid Missing
1 neighbourhood_group [character]
1. 01. Centro
2. 02. Arganzuela
3. 03. Retiro
4. 04. Salamanca
5. 05. Chamartín
6. 06. Tetuán
7. 07. Chamberí
8. 08. Fuencarral-El Pardo
9. 09. Moncloa-Aravaca
10. 10. Latina
[ 11 others ]
1(4.8%)
1(4.8%)
1(4.8%)
1(4.8%)
1(4.8%)
1(4.8%)
1(4.8%)
1(4.8%)
1(4.8%)
1(4.8%)
11(52.4%)
21 (100.0%) 0 (0.0%)
2 population [numeric]
Mean (sd) : 157729 (56921)
min ≤ med ≤ max:
50077 ≤ 146016 ≤ 258633
IQR (CV) : 72394 (0.4)
21 distinct values 21 (100.0%) 0 (0.0%)

Generated by summarytools 1.0.0 (R version 4.1.1)
2021-12-27

  • Dimensiones: 21 registros x 2 variables.
  • Duplicados: 0.
  • Valores nulos (columna Missing): no hay valores perdidos.

Hay 1 variable discreta de tipo character:

  • neighbourhood_group : distrito.

Y 1 variable numérica:

  • population: número de habitantes.

Este dataset se empleará para calcular la densidad de población por alojamiento turístico.


2 Limpieza de los datos


Una vez cargados los datos y analizado el contenido de cada dataset, se procede a su limpieza y normalización.

2.1 Dataframe df_listings: Detección y gestión de datos que contienen ceros o elementos vacíos

# Guardamos número de filas antes de la limpieza
dim_ini <- nrow(df_listings)

Se comprueba la existencia de elementos vacíos.

# ‘Not Available’ / Missing Values
colSums(is.na(df_listings))
##                             id                           name 
##                              0                              0 
##                        host_id                      host_name 
##                              0                              0 
##            neighbourhood_group                  neighbourhood 
##                              0                              0 
##                       latitude                      longitude 
##                              0                              0 
##                      room_type                          price 
##                              0                              0 
##                 minimum_nights              number_of_reviews 
##                              0                              0 
##                    last_review              reviews_per_month 
##                              0                           5637 
## calculated_host_listings_count               availability_365 
##                              0                              0

Se observa que reviews_per_month es la única variable a la que le faltan valores. Que no haya reseñas no parece ser un error y se puede considerar un valor legítimo, por lo que se sustituyen los valores no disponibles por 0.

df_listings$reviews_per_month[which(is.na(df_listings$reviews_per_month))] <- 0
# Comprobamos que tras la sustitución ya no hay elementos vacíos
colSums(is.na(df_listings))
##                             id                           name 
##                              0                              0 
##                        host_id                      host_name 
##                              0                              0 
##            neighbourhood_group                  neighbourhood 
##                              0                              0 
##                       latitude                      longitude 
##                              0                              0 
##                      room_type                          price 
##                              0                              0 
##                 minimum_nights              number_of_reviews 
##                              0                              0 
##                    last_review              reviews_per_month 
##                              0                              0 
## calculated_host_listings_count               availability_365 
##                              0                              0

Se comprueba si existen elementos con valores en blanco.

colSums(df_listings=="")
##                             id                           name 
##                              0                              3 
##                        host_id                      host_name 
##                              0                            527 
##            neighbourhood_group                  neighbourhood 
##                              0                              0 
##                       latitude                      longitude 
##                              0                              0 
##                      room_type                          price 
##                              0                              0 
##                 minimum_nights              number_of_reviews 
##                              0                              0 
##                    last_review              reviews_per_month 
##                           5637                              0 
## calculated_host_listings_count               availability_365 
##                              0                              0

Existen elementos con valores en blanco (name, host_name y last_review), pero no los vamos a tratar porque estas variables no se van a utilizar en el análisis posterior, a excepción de name, pero el hecho de que tenga tres valores en blanco no afecta al estudio.

Se comprueba si existen elementos con valores iguales a cero.

colSums(df_listings==0)
##                             id                           name 
##                              0                              0 
##                        host_id                      host_name 
##                              0                              0 
##            neighbourhood_group                  neighbourhood 
##                              0                              0 
##                       latitude                      longitude 
##                              0                              0 
##                      room_type                          price 
##                              0                              8 
##                 minimum_nights              number_of_reviews 
##                              0                           5637 
##                    last_review              reviews_per_month 
##                              0                           5637 
## calculated_host_listings_count               availability_365 
##                              0                           5494

Las variables number_of_reviews, reviews_per_month, availability_365 y price tienen valores a cero.

Para number_of_reviews, 0 se puede considerar un valor legítimo (no hay reseñas).

reviews_per_month la tratamos anteriormente y se determinó que 0 se podía considerar un valor legítimo. Si no hay reseñas, el acumulado está también a cero.

En el caso de availability_365, tal y como indicaba el diccionario de datos, hay que tener en cuenta que un anuncio puede no estar disponible porque ha sido reservado por un invitado o bloqueado por el anfitrión, por lo que cero se puede considerar un valor legítimo.

Sin embargo, para price, el valor cero no parece tener sentido, ya que se estaría hablando de alojamiento gratuito. En este caso, no se tendrán en cuenta los registros con price=0.

dim(df_listings)
## [1] 19618    16
df_listings <- df_listings[!(df_listings$price == 0),]
# Comprobamos que se han eliminado registros
dim(df_listings)
## [1] 19610    16

2.2 Dataframe df_listings: Identificación y tratamiento de valores extremos

En la descripción del dataset, se observan dos variables con valores que llamaban la atención por tomar valores muy altos: price y minimum_nights.

price: el precio máximo es 9999, lo que podría estar indicando que la ausencia de valores se ha podido codificar como 9999 o que se trata de un error en el scrape. Estos valores aparecerán como outliers.

ggplot() + 
  geom_boxplot(aes(y = df_listings$price), fill='#4271AE', outlier.colour='red', alpha=0.9) + 
  scale_x_discrete( ) + 
  labs(title = "Precio diario en euros", y = "Precio") +
  coord_flip()

Se eliminan los outliers por distrito. Para ello se determina un límite de precio máximo en el percentil 95 de los precios de cada distrito, y se eliminan todos los registros por encima de ese valor.

# Se define una función para hacer la limpieza
clean_price <- function(district_name){
  district <- df_listings[df_listings$neighbourhood_group==district_name,]
  district.quantile <- quantile(district$price, probs=c(.95), na.rm=T)
  df_listings <- df_listings[df_listings$neighbourhood_group!=district_name | (df_listings$neighbourhood_group==district_name & df_listings$price<district.quantile),]
  return(df_listings)
}

dim(df_listings)
## [1] 19610    16
# Se limpian todos los distritos
df_listings = clean_price("Centro")
df_listings = clean_price("Arganzuela") 
df_listings = clean_price("Retiro") 
df_listings = clean_price("Salamanca") 
df_listings = clean_price("Chamartín") 
df_listings = clean_price("Tetuán") 
df_listings = clean_price("Chamberí") 
df_listings = clean_price("Fuencarral - El Pardo") 
df_listings = clean_price("Moncloa - Aravaca") 
df_listings = clean_price("Latina") 
df_listings = clean_price("Carabanchel") 
df_listings = clean_price("Usera") 
df_listings = clean_price("Puente de Vallecas") 
df_listings = clean_price("Moratalaz") 
df_listings = clean_price("Ciudad Lineal") 
df_listings = clean_price("Hortaleza") 
df_listings = clean_price("Villa de Vallecas") 
df_listings = clean_price("Vicálvaro") 
df_listings = clean_price("San Blas - Canillejas") 
df_listings = clean_price("Barajas")
df_listings = clean_price("Villaverde")

dim(df_listings)
## [1] 18599    16

Tras la limpieza se obtiene el siguiente boxplot:

ggplot() + 
  geom_boxplot(aes(y = df_listings$price), fill='#4271AE', outlier.colour='red', alpha=0.9) + 
  scale_x_discrete( ) + 
  labs(title ="Precio diario en euros", y = "Precio") +
  coord_flip()

Obtenemos el precio medio por distrito y ordenamos por precio.

# Obtenemos precio medio por distrito
df_mean_price_by_neighbourhood_group <- aggregate(df_listings$price, list(df_listings$neighbourhood_group), FUN=mean) 
# Ordenamos por precio
df_mean_price_by_neighbourhood_group[order(df_mean_price_by_neighbourhood_group[,2]),]
##                  Group.1         x
## 13    Puente de Vallecas  38.64444
## 21            Villaverde  39.23810
## 18                 Usera  42.15789
## 3            Carabanchel  46.45455
## 10                Latina  47.01560
## 20     Villa de Vallecas  53.80645
## 1             Arganzuela  54.41459
## 12             Moratalaz  59.41441
## 7          Ciudad Lineal  60.93496
## 17                Tetuán  66.44890
## 2                Barajas  69.46584
## 8  Fuencarral - El Pardo  69.82095
## 14                Retiro  70.05096
## 11     Moncloa - Aravaca  72.18821
## 4                 Centro  75.26788
## 6               Chamberí  76.22035
## 5              Chamartín  79.75410
## 9              Hortaleza  85.80290
## 15             Salamanca  94.23665
## 19             Vicálvaro  96.04412
## 16 San Blas - Canillejas 242.86017

Se observa que Puente de Vallecas es el más barato y que San Blas - Canillejas y Vicálvaro son los más caros. Estos resultados son bastante extraños, ya que esas no son las zonas más caras de la capital.

¿Qué ocurre con San Blas - Canillejas y Vicálvaro?

df_listings %>%
  ggplot(aes(x=neighbourhood_group, y=price, fill=neighbourhood_group))+
  geom_boxplot(show.legend=FALSE)+
  coord_flip()

En el diagrama de cajas se aprecia un gran número de valores extremos en el distrito de San Blas-Canillejas. Hay un motivo principal para esta distribución tan anómala, que también afecta en menor medida a Vicálvaro , Moratalaz, Hortaleza, Fuencarral - El Pardo, Ciudad Lineal, y Barajas, y es su cercanía al estadio Wanda Metropolitano, ya que debido a la celebración de la final de la Champions League en ese estadio en 2019 los precios se dispararon en la zona. Este hecho se ve reflejado en la variable price, que contiene el mayor precio al que se alquila (o alquiló) el alojamiento turístico.

Para solucionar este problema vamos a considerar valores extremos en estos distritos a cualquier valor por encima de 150 euros.

df_listings <- df_listings[!(df_listings$neighbourhood_group=="San Blas - Canillejas" & df_listings$price>150),]
df_listings <- df_listings[!(df_listings$neighbourhood_group=="Vicálvaro" & df_listings$price>150),]
df_listings <- df_listings[!(df_listings$neighbourhood_group=="Moratalaz" & df_listings$price>150),]
df_listings <- df_listings[!(df_listings$neighbourhood_group=="Hortaleza" & df_listings$price>150),]
df_listings <- df_listings[!(df_listings$neighbourhood_group=="Fuencarral - El Pardo" & df_listings$price>150),]
df_listings <- df_listings[!(df_listings$neighbourhood_group=="Ciudad Lineal" & df_listings$price>150),]
df_listings <- df_listings[!(df_listings$neighbourhood_group=="Barajas" & df_listings$price>150),]

# Obtenemos precio medio por distrito
df_mean_price_by_neighbourhood_group <- aggregate(df_listings$price, list(df_listings$neighbourhood_group), FUN=mean) 
# Ordenamos por precio
df_mean_price_by_neighbourhood_group[order(df_mean_price_by_neighbourhood_group[,2]),]
##                  Group.1        x
## 19             Vicálvaro 36.86792
## 13    Puente de Vallecas 38.64444
## 21            Villaverde 39.23810
## 12             Moratalaz 40.02885
## 18                 Usera 42.15789
## 7          Ciudad Lineal 43.37610
## 3            Carabanchel 46.45455
## 10                Latina 47.01560
## 2                Barajas 48.69178
## 8  Fuencarral - El Pardo 50.25092
## 16 San Blas - Canillejas 50.75503
## 20     Villa de Vallecas 53.80645
## 1             Arganzuela 54.41459
## 9              Hortaleza 54.67213
## 17                Tetuán 66.44890
## 14                Retiro 70.05096
## 11     Moncloa - Aravaca 72.18821
## 4                 Centro 75.26788
## 6               Chamberí 76.22035
## 5              Chamartín 79.75410
## 15             Salamanca 94.23665

Se aprecia tras corregir como los distritos situados dentro de la almendra central son los más caros, con la excepción de Moncloa - Aravaca que es una de las zonas de lujo de la capital.

ggplot() + 
  geom_boxplot(aes(y = df_listings$price), fill='#4271AE', outlier.colour='red', alpha=0.9) + 
  scale_x_discrete( ) + 
  labs(title ="Precio diario en euros", y = "Precio") +
  coord_flip()

minimum_nights: el valor máximo toma un valor excesivamente alto (1125), lo que podría estar indicando una captura errónea del dato. Estos valores aparecerán como outliers.

ggplot() + 
  geom_boxplot(aes(y = df_listings$minimum_nights), fill='#4271AE', outlier.colour='red', alpha=0.9) + 
  scale_x_discrete( ) + 
  labs(title = "Número mínimo de noches", y = "Número de noches") + 
  coord_flip()

El Plan Especial de Hospedaje (PEH), que entró en vigor tras su aprobación definitiva en el Pleno municipal del 27 de marzo de 2019, limita a 90 días la posibilidad de alquilar una vivienda con fines turísticos sin permiso y a partir de ese plazo obliga a obtener una licencia de uso terciario de hospedaje. Esta medida afecta al 95% de los pisos de uso turístico.

Se decide no eliminar registros, en este caso de valores superiores a 90, sino limitar estos registros a un máximo de 90 noches al año.

df_listings$minimum_nights[df_listings$minimum_nights > 90] <- 90

Tras la limpieza se obtiene el siguiente boxplot:

ggplot() + 
  geom_boxplot(aes(y = df_listings$minimum_nights), fill='#4271AE', outlier.colour='red', alpha=0.9) + 
  scale_x_discrete( ) + 
  labs(title = "Número mínimo de noches", y = "Número de noches") + 
  coord_flip()

Los outliers se consideran válidos al haber propietarios que requieran mayor estabilidad en el alquiler.

Tras el proceso de limpieza, tenemos el siguiente balance:

num_deleted_rows <- dim_ini - nrow(df_listings)
print(sprintf("El número total de filas eliminadas es %d, lo que representa el %.2f %% del total de los datos del dataset.", num_deleted_rows, num_deleted_rows*100/dim_ini))
## [1] "El número total de filas eliminadas es 1341, lo que representa el 6.84 % del total de los datos del dataset."

Se ha eliminado un porcentaje bajo de registros. Se entiende que los outliers se han debido a problemas en el scraping al generar el dataset.

2.3 Dataframe df_reviews: Detección y gestión de datos que contienen ceros o elementos vacíos

# Guardamos número de filas antes de la limpieza
dim_ini <- nrow(df_reviews)

Se comprueba si existen elementos vacíos.

# ‘Not Available’ / Missing Values
colSums(is.na(df_reviews))
##    listing_id            id          date   reviewer_id reviewer_name 
##             0             0             0             0             0 
##      comments 
##             4

Se eliminan las reseñas sin comentarios.

dim(df_reviews)
## [1] 625006      6
df_reviews <- df_reviews[!is.na(df_reviews$comments),]
# Comprobamos que se han eliminado registros
dim(df_reviews)
## [1] 625002      6

Se comprueba la existencia de elementos con valores en blanco.

colSums(df_reviews=="")
##    listing_id            id          date   reviewer_id reviewer_name 
##             0             0             0             0             1 
##      comments 
##           329

Se eliminan las reseñas con valores en blanco.

dim(df_reviews)
## [1] 625002      6
df_reviews <- df_reviews[!(df_reviews$comments == ""),]
# Comprobamos que se han eliminado registros
dim(df_reviews)
## [1] 624673      6

Se observa un registro con reviewer_name en blanco, pero no se trata porque no afecta al estudio.

2.4 Dataframe df_reviews: Identificación y tratamiento de valores extremos

Se decide no hacer este estudio por la naturaleza de los datos (identificadores, fechas y texto libre).

Tras el proceso de limpieza, se tiene el siguiente balance:

num_deleted_rows <- dim_ini - nrow(df_reviews)
print(sprintf("El número total de filas eliminadas es %d, lo que representa el %.2f %% del total de los datos del dataset.", num_deleted_rows, num_deleted_rows*100/dim_ini))
## [1] "El número total de filas eliminadas es 333, lo que representa el 0.05 % del total de los datos del dataset."

2.5 Otros tratamientos

2.5.1 Dataframe df_population: Normalización de los nombres de los distritos

Se limpia el dataframe para posteriormente poder unir esos datos con los de df_listing usando neighbourhood_group.

# Eliminamos  los cuatro primeros caracteres del campo neighbourhood_group
df_population$neighbourhood_group <- substring(df_population$neighbourhood_group, first = 5)
# Reemplazamos los guiones por guiones separados por espacios
df_population$neighbourhood_group <- gsub('-',' - ',df_population$neighbourhood_group)
tail(df_population)
##      neighbourhood_group population
## 16             Hortaleza     193228
## 17            Villaverde     154808
## 18     Villa de Vallecas     114733
## 19             Vicálvaro      75485
## 20 San Blas - Canillejas     160258
## 21               Barajas      50077

3 Análisis de los datos


La tercera fase del ciclo de vida de los datos, después de su captura y tratamiento es su análisis con el objeto de extraer información de los mismos.

3.1 Selección de los grupos de datos que se quieren analizar/comparar (planificación de los análisis a aplicar)

Para contestar a las diferentes preguntas se requieren distintos tipos de análisis. A continuación, se muestra una tabla con los diferentes análisis a realizar:

Pregunta Tipo de análisis Variables analizadas Visualización
¿Qué distritos tienen más alojamientos? Univariante listing.csv: neighbourhood_group Plot
¿Qué tipo de alojamiento es el más frecuente? Univariante listing.csv: room_type Plot
¿Cuáles son las palabras más utilizadas en el título de los anuncios de alojamientos? Univariante listing.csv: name Nube de palabras
¿Cuál es el precio medio para cada tipo de alojamiento? Bivariante listing.csv: price, room_type Tabla
¿Qué tipo de alojamiento es el más frecuente por distrito? Bivariante listing.csv: neighbourhood_group, room_type Plot
¿Existen diferencias significativas de precio para los diferentes distritos? Bivariante. Test de normalidad, test de homocedasticidad, test de hipótesis. listing.csv: id, neighbourhood_group, price, price_group (categorización de price) Tabla, Plot
¿Cuantos alojamientos por tipo de habitación y precio hay? ¿Existen diferencias significativas de precio para los diferentes tipos de alojamiento? Bivariante. Test de normalidad, test de homocedasticidad, test de hipótesis. listing.csv: room_type, price, price_group (categorización de price) Plot
¿Cuál es la densidad de alojamientos por distrito? ¿Qué distritos tienen una mayor densidad de alojamientos por habitante? Multivariante C5000121.xls:neighbourhood_group, population - listing.csv: neighbourhood_group, longitude,latitude Plot, Diagrama de densidad
¿Existe una diferencia significativa entre el promedio de reseñas por mes para los alojamientos de tipo habitación privada y apartamento? Bivariante: Test de normalidad, test de homocedasticidad, test de hipótesis. listing.csv: reviews_per_month, room_type
¿Existe una diferencia significativa entre los tipos de alojamiento por distrito? Bivariante: test chi cuadrado listing.csv: neighbourhood_group, room_type
¿Se podría construir un modelo de regresión para predecir el precio del alojamiento en función de otras variables? Multivariante listing.csv: price, minimum_nights, reviews_per_month, availability_365, latitude, longitude Plot de correlación
¿Cómo es la estacionalidad en el alquiler de alojamientos turísticos? Univariante reviews_detailed.csv: date Plot

3.2 Selección de datos importantes y exportación a fichero

Una vez realizada la limpieza y determinados los datos que se necesitan para el análisis, se guardan.

df_listings <- df_listings[, c("id", "name", "neighbourhood_group", "latitude", "longitude", "room_type", "price", "minimum_nights", "reviews_per_month", "availability_365")]
write.csv(df_listings, "listings_clean.csv")

df_reviews <- df_reviews[, c("listing_id", "date")]
write.csv(df_listings, "reviews_detailed_clean.csv")

write.csv(df_population, "population_clean.csv")

3.3 Pruebas estadísticas

3.3.1 ¿Qué distritos tienen más alojamientos?

# Mostramos plot con distritos ordenados por frecuencia
neighbourhood_group_freq <- data.frame(table(df_listings$neighbourhood_group))
ggplot(neighbourhood_group_freq, aes(x = reorder(Var1, -Freq), y = Freq)) +
  geom_segment(aes(x = reorder(Var1, - Freq), xend = reorder(Var1, - Freq), y = 0, yend = Freq), color = "gray", lwd = 1) +
  geom_point(size = 6, pch = 21, bg = 4, col = 1) +
  geom_text(aes(label=Freq), color = "white", size = 2) +
  xlab("Distrito") +
  ylab("Número de anuncios") +
  coord_flip()+
  theme_minimal()

Se observa que el distrito Centro es con mucha diferencia el que más anuncios tiene. Los siguientes distritos Salamanca, Chamberí, Arganzuela y Tetúan son distritos limítrofes al centro. Están todos en el interior de la M-30.

3.3.2 ¿Qué tipo de alojamiento es el más frecuente?

# Mostramos plot con alojamientos ordenados por frecuencia
room_type_freq <- data.frame(table(df_listings$room_type))
ggplot(room_type_freq, aes(x = reorder(Var1, -Freq), y = Freq)) +
  geom_segment(aes(x = reorder(Var1, - Freq), xend = reorder(Var1, - Freq), y = 0, yend = Freq), color = "gray", lwd = 1) +
  geom_point(size = 8, pch = 21, bg = 4, col = 1) +
  geom_text(aes(label=Freq), color = "white", size = 2) +
  xlab("Tipo de habitación") +
  ylab("Número de anuncios") +
  coord_flip()+
  theme_minimal()

El tipo de habitación más frecuente es Entire home/apt, seguido de Private room. Los usuarios de Airbnb dan mucha importancia a su privacidad, de ahí los bajos valores de las habitaciones compartidas. El resultado de las habitaciones de hotel es testimonial, ya que este tipo de alojamiento se ofertan en otras plataformas.

3.3.3 ¿Cuáles son las palabras utilizadas en el título de los anuncios de alojamientos?

Se muestra el resultado con una nube de palabras.

#Generamos el corpus a partir del campo name 
wordlist <- strsplit(df_listings$name," ")
wordlist <- as.VCorpus(wordlist)

#Normalizamos los nombres
wordlist <- tm_map(wordlist, tolower)
wordlist <- tm_map(wordlist, removePunctuation)
wordlist <- tm_map(wordlist, removeNumbers)
wordlist <- tm_map(wordlist, removeWords, stopwords(kind='en'))
wordlist <- tm_map(wordlist, removeWords, stopwords(kind='es'))
wordlist <- tm_map(wordlist, PlainTextDocument)

#Creamos el wordcloud
myDTM <- TermDocumentMatrix(wordlist, control = list(minWordLength=1))
m <- as.matrix(myDTM)
v <- sort(rowSums(m), decreasing=TRUE)
wordcloud(names(v),v, min.freq=50, colors=brewer.pal(6,"Dark2"),random.order=FALSE)

Las palabras más utilizadas en los títulos de los anuncios de alojamientos de Airbnb en Madrid son habitación, apartamento y centro, con sus variantes en inglés.

3.3.4 ¿Qué tipo de alojamiento es el más frecuente por distrito?

ggplot(df_listings,aes(neighbourhood_group ,fill=room_type)) + 
  geom_bar() +labs(x="Distrito", y="Tipo de alojamiento") + 
  guides(fill=guide_legend(title="")) + 
  ggtitle("Alojamientos por distrito") + 
  coord_flip()

ggplot(df_listings, aes(fill=room_type, x=neighbourhood_group, y = price))+
  geom_bar(position="fill",stat="identity") +
  labs(y= 'Alojamiento', x = 'Distrito') +
  guides(fill=guide_legend(title=""))+
  coord_flip()

Con la excepción de Villaverde, Villa de Vallecas, Vicálvaro, San Blas - Canillejas, Moratalaz,Barajas y Ciudad Lineal, en el resto de Madrid predomina el alquiler del apartamento completo en vez de habitaciones privadas o compartidas.

3.3.5 ¿Existen diferencias significativas de precio para los diferentes distritos?

En los siguientes gráficos se observa la distribución de precios por distrito. Dividimos el precio en 5 grupos: Muy bajo, Bajo, Moderado, Alto y Muy alto.

# Obtenemos cuantiles
quantile_price = integer(10)
for (i in 1:10) {
  quantile_price[i] = quantile(df_listings$price,i * 0.1) 
}
quantile_price
##  [1]  20.6  30.0  36.0  45.0  54.0  65.0  79.0  99.0 135.0 380.0
# Creamos campo nuevo con precio categorizado
df_listings <- df_listings %>% mutate(price_group=ifelse(price < quantile_price[2]+1, "Muy bajo",
                                                ifelse(price < quantile_price[4]+1, "Bajo",
                                                ifelse(price < quantile_price[6]+1, "Moderado",
                                                ifelse(price < quantile_price[8]+1, "Alto", "Muy alto"
                                                 )))))
ggplot(df_listings, aes(neighbourhood_group)) + 
  geom_bar(aes(fill = price_group)) + 
  ggtitle("Listings per price and neighbourhood group") +
  guides(fill=guide_legend(title="")) +
  coord_flip()

ggplot(df_listings, aes(x=neighbourhood_group, y=id, fill=price_group ))+
  geom_bar(position="fill", stat="identity")+
  labs(y="Price", x="Neighbourhood group")+
  guides(fill=guide_legend(title=""))+
  coord_flip()

En este último gráfico se aprecia la diferencia entre los precios por distritos. Mientras podemos encontrar alojamientos de cualquier tipo de precio en los distritos de la almendra central, los precios más baratos siempre están en el extrarradio.

pal <- colorNumeric(palette=rainbow(6), domain=df_listings$price)
leaflet(data=df_listings)%>%
  addProviderTiles(providers$CartoDB.Positron)%>%
  addCircleMarkers(~longitude, ~latitude, color=~pal(price), weight=1, radius=1.5, fillOpacity=1, opacity=1,
                   label=paste("Neighbourhood:", df_listings$neighbourhood_group))%>%
  addLegend("bottomright", pal = pal, values =~price,
            title="Precio",
            opacity=1,)

Se comprueba si el precio sigue una distribución normal cont el test de Kolmogorov-Smirnov:

is_normal_distribution <- function(x){ifelse(x >= 0.05, "Distribution normal", "NO es distribution normal")}
result <- ks.test(df_listings$price, pnorm, mean(df_listings$price), sd(df_listings$price))
## Warning in ks.test(df_listings$price, pnorm, mean(df_listings$price),
## sd(df_listings$price)): ties should not be present for the Kolmogorov-Smirnov
## test
is_normal_distribution(result$p.value)
## [1] "NO es distribution normal"

Como la distribución no es normal, se comprueba la homocedasticidad del precio por distrito con el test de Fligner-Killeen (alternativa no paramétrica, utilizada cuando los datos no cumplen con la condición de normalidad):

is_homogeneous_variance <- function(x){ifelse(x >= 0.05, "Varianza homogénea", "Varianza NO homogénea")}

result <- fligner.test(price ~ neighbourhood_group, data = df_listings)
is_homogeneous_variance(result$p.value)
## [1] "Varianza NO homogénea"

La variable price presenta varianzas estadísticamente diferentes para los diferentes distritos (neighbourhood_group).

La variable price no sigue una distribución normal y presenta varianzas estadísticamente diferentes para los diferentes distritos. Por este motivo, se usa el test de Kruskal-Wallis, que es la alternativa no paramétrica a los contrastes de hipótesis de más de 2 grupos para comprobar si el precio muestra diferencias significativas para los diferentes distritos.

kruskal.test(price ~ neighbourhood_group, data = df_listings)
## 
##  Kruskal-Wallis rank sum test
## 
## data:  price by neighbourhood_group
## Kruskal-Wallis chi-squared = 1959.6, df = 20, p-value < 2.2e-16

El p-valor obtenido es menor al nivel de significancia (0.05), por tanto, se puede concluir que el precio (price) muestra diferencias significativas para los diferentes distritos (neighbourhood_group).

3.3.6 ¿Cuál es el precio medio de cada tipo de alojamiento?

mean_price_table <- aggregate(df_listings$price, list(df_listings$room_type), FUN=mean)
# Ordenamos por precio
mean_price_table[order(mean_price_table$x), ]
##           Group.1        x
## 4     Shared room 35.37829
## 3    Private room 40.06133
## 1 Entire home/apt 88.40722
## 2      Hotel room 90.42105
ggplot(df_listings, aes(x=price, fill=room_type))+
  geom_density(alpha=0.5)

Un alquiler de un apartamento completo tiene un precio similar al de una habitación de hotel, y es un 220% más caro que una habitación privada y un 250% más caro que una habitación compartida.

3.3.7 ¿Cuantos alojamientos por tipo de habitación y precio hay? ¿Existen diferencias significativas de precio para los diferentes tipos de habitación?

ggplot(df_listings, aes(room_type)) + geom_bar(aes(fill = price_group)) + ggtitle("Alojamientos por tipo de habitación y precio")

Se aprecia que los apartamentos completos tienen un rango de precios entre alto y muy alto. Las habitaciones privadas suelen ser de precio muy bajo - bajo, siendo las habitaciones compartidas generalmente de precio muy bajo.

Anteriormente, se comprobó que el precio no seguía una distribución normal. Se comprueba ahora la homocedasticidad del precio por tipo de habitación con el test de Fligner-Killeen (alternativa no paramétrica, utilizada cuando los datos no cumplen con la condición de normalidad):

result <- fligner.test(price ~ room_type, data = df_listings)
is_homogeneous_variance(result$p.value)
## [1] "Varianza NO homogénea"

La variable price presenta varianzas estadísticamente diferentes para los diferentes tipos de habitación (room_type).

La variable price no sigue una distribución normal y presenta varianzas estadísticamente diferentes para los diferentes tipos de habitación. Por este motivo, se aplica el test de Kruskal-Wallis, que es la alternativa no paramétrica a los contrastes de hipótesis de más de 2 grupos para comprobar si el precio muestra diferencias significativas para los diferentes tipos de habitación.

kruskal.test(price ~ room_type, data = df_listings)
## 
##  Kruskal-Wallis rank sum test
## 
## data:  price by room_type
## Kruskal-Wallis chi-squared = 7606.6, df = 3, p-value < 2.2e-16

El p-valor obtenido es menor al nivel de significancia (0.05), por tanto, se puede concluir que el precio (price) muestra diferencias significativas para los diferentes tipos de habitación (room_type).

3.3.8 ¿Qué distritos tienen una mayor densidad de alojamientos por habitante?

Para responder a esta pregunta se necesitan los datos de población por distrito cargados en el dataframe df_population.

Se unen los datos de población a la tabla de frecuencias de anuncios por distrito calculada anteriormente.

# Renombramos columnas de la tabla de frecuencias
colnames(neighbourhood_group_freq) <- c('neighbourhood_group', 'listings')
# Unimos datos de población
neighbourhood_group_freq <- join(neighbourhood_group_freq, df_population, type ='left')
## Joining by: neighbourhood_group

Se crea una nueva variable que almacene el número de habitantes por anuncio.

neighbourhood_group_freq$density <- round(neighbourhood_group_freq$population / neighbourhood_group_freq$listings, 0)
tail(neighbourhood_group_freq)
##      neighbourhood_group listings population density
## 16 San Blas - Canillejas      298     160258     538
## 17                Tetuán      773     159849     207
## 18                 Usera      266     142454     536
## 19             Vicálvaro       53      75485    1424
## 20     Villa de Vallecas       93     114733    1234
## 21            Villaverde      168     154808     921

Y se visualizan los datos.

ggplot(neighbourhood_group_freq, aes(x = reorder(neighbourhood_group, -density), y = density)) +
  geom_segment(aes(x = reorder(neighbourhood_group, - density), xend = reorder(neighbourhood_group, - density), y = 0, yend = density), color = 'gray', lwd = 1) +
  geom_point(size = 6, pch = 21, bg = 4, col = 1) +
  geom_text(aes(label=density), color = 'white', size = 2) +
  xlab('District') +
  ylab('Número de habitantes por anuncio y distrito') +
  coord_flip()+
  theme_minimal()

En el distrito Centro hay un alojamiento por cada 17 habitantes. Los 4 siguientes distritos con menor número de habitantes por alojamiento turísticos están todos en la almendra central. En Vicálvaro hay un alojamiento por cada 1424 habitantes. Todos los distritos con mayor número de habitantes por alojamiento turísticos están en el extrarradio.

Se genera un mapa para visualizar los resultados geográficamente. Se aplica una normalización logarítmica para reducir el rango de valores.

df_listings <- merge(df_listings, neighbourhood_group_freq, type='left')
ggplot(data=df_listings) +
  geom_point(aes(x=latitude, y=longitude, color = log10(density))) +
  ggtitle('Densidad de alojamientos turísticos por habitante')

El mapa nos permite apreciar la mayor densidad de alojamientos turísticos en la zona central de Madrid.

3.3.9 ¿Existe una diferencia significativa entre el promedio de reseñas por mes para los alojamientos de tipo habitación privada y apartamento?

Se seleccionan los registros para los alojamientos con habitación privada y apartamento completo.

df_aux <- subset(df_listings, room_type=="Private room" | room_type=="Entire home/apt")
head(df_aux)
##   neighbourhood_group       id
## 1          Arganzuela 42815572
## 2          Arganzuela 13745016
## 3          Arganzuela 10533378
## 4          Arganzuela 16358569
## 5          Arganzuela 40705179
## 6          Arganzuela 47404180
##                                                 name latitude longitude
## 1                          Tu casa en los Manzanares 40.40578  -3.71919
## 2                Bonito y céntrico piso. Hasta 4 pax 40.41203  -3.72005
## 3             Apartament in Center of Madrid, Atocha 40.40038  -3.69491
## 4                           APARTAMENTO EN EL CENTRO 40.40061  -3.69668
## 5 Centric, Spacious & Beautiful 3-Bedrooms Apartment 40.40113  -3.69352
## 6                                         Delicias I 40.39952  -3.70038
##         room_type price minimum_nights reviews_per_month availability_365
## 1    Private room    20              1              0.00              156
## 2 Entire home/apt    50              2              0.17                0
## 3 Entire home/apt    43              2              4.48              165
## 4 Entire home/apt    58              1              0.25              364
## 5 Entire home/apt   108              2              0.25               73
## 6 Entire home/apt    37              2              3.46              171
##   price_group listings population density
## 1    Muy bajo     1042     154243     148
## 2    Moderado     1042     154243     148
## 3        Bajo     1042     154243     148
## 4    Moderado     1042     154243     148
## 5    Muy alto     1042     154243     148
## 6        Bajo     1042     154243     148

Se comprueba la normalidad de la distribución para las reseñas por mes (reviews_per_month) con el test de Kolmogorov-Smirnov.

result <- ks.test(df_aux$reviews_per_month, pnorm, mean(df_aux$reviews_per_month), sd(df_aux$reviews_per_month))
## Warning in ks.test(df_aux$reviews_per_month, pnorm,
## mean(df_aux$reviews_per_month), : ties should not be present for the Kolmogorov-
## Smirnov test
is_normal_distribution(result$p.value)
## [1] "NO es distribution normal"

Como la distribución no es normal, se comprueba la homocedasticidad de las reseñas mensuales por tipo de habitación con el test de Fligner-Killeen (alternativa no paramétrica, utilizada cuando los datos no cumplen con la condición de normalidad):

result <- fligner.test(reviews_per_month ~ room_type, data = df_aux)
is_homogeneous_variance(result$p.value)
## [1] "Varianza NO homogénea"

La variable reviews_per_month presenta varianzas estadísticamente diferentes para los diferentes tipos de habitación (room_type).

No se cumplen las condiciones de normalidad, ni de homocedasticidad, por lo que se usa el test de Kruskal-Wallis, que es la alternativa no paramétrica a los contrastes de hipótesis con el objetivo de comprobar si las reseñas por mes muestran diferencias significativas para los tipos de habitación seleccionados.

kruskal.test(reviews_per_month ~ room_type, data = df_aux)
## 
##  Kruskal-Wallis rank sum test
## 
## data:  reviews_per_month by room_type
## Kruskal-Wallis chi-squared = 806.91, df = 1, p-value < 2.2e-16

El p-valor obtenido es menor al nivel de significancia (0.05), por tanto, se puede concluir que las reseñas por mes (reviews_per_month) muestran diferencias significativas para los tipos de habitación seleccionados (room_type).

3.3.10 ¿Existe una diferencia significativa entre los tipos de habitación por distrito?

Para comparar si existen diferencias significativas en una variable categórica entre los grupos definidos por otra variable categórica, se puede aplicar el test chi cuadrado, mediante la función chisq.test().

Se construye la tabla de contingencia para ambas variables.

# Tabla de contingencia room_type y neighbourhood_group
conting <- table(df_listings$neighbourhood_group, df_listings$room_type)
conting
##                        
##                         Entire home/apt Hotel room Private room Shared room
##   Arganzuela                        532          1          497          12
##   Barajas                            43          0          101           2
##   Carabanchel                       251          0          415           5
##   Centro                           5691        100         2239         179
##   Chamartín                         327          4          208          10
##   Chamberí                          655          8          504          22
##   Ciudad Lineal                     174          1          387           7
##   Fuencarral - El Pardo             104          0          166           1
##   Hortaleza                         124          5          175           1
##   Latina                            207          0          359          11
##   Moncloa - Aravaca                 266          2          257           1
##   Moratalaz                          23          0           79           2
##   Puente de Vallecas                223          0          356           6
##   Retiro                            367          1          257           3
##   Salamanca                         865         10          370          10
##   San Blas - Canillejas             108          0          187           3
##   Tetuán                            446          0          319           8
##   Usera                              84          0          175           7
##   Vicálvaro                           8          0           45           0
##   Villa de Vallecas                  22          0           70           1
##   Villaverde                         32          1          122          13

Se aplica el test chi cuadrado.

chisq.test(conting)
## Warning in chisq.test(conting): Chi-squared approximation may be incorrect
## 
##  Pearson's Chi-squared test
## 
## data:  conting
## X-squared = 1835, df = 60, p-value < 2.2e-16

El p-valor obtenido es menor al nivel de significancia (0.05), por tanto, se puede concluir que el tipo de habitación (room_type) muestra diferencias significativas para los diferentes distritos (neighbourhood_group).

3.3.11 ¿Cómo es la estacionalidad en el alquiler de alojamientos turísticos?

Para responder a esta pregunta hay que saber en qué momento del año se han producido los alquileres. Este dato no está disponible, pero se puede utilizar una aproximación sustituyendolo por la fecha en la que se ha hecho una reseña. Para ello se aplica el dataset reviews_detailed.csv que contiene un inventario de todas las reseñas realizadas.

Se convierte el campo date a tipo de datos fecha y se divide la fecha en año, mes, número de día y día de la semana.

reviews_count <- df_reviews$date
reviews_count <- as.Date(reviews_count, "%Y-%m-%d")
reviews_count <- data.frame(date = reviews_count,
                            year = as.numeric(format(reviews_count, format = "%Y")),
                            month = as.numeric(format(reviews_count, format = "%m")),
                            day = as.numeric(format(reviews_count, format = "%d")))
reviews_count$weekday <- strftime(reviews_count$date, '%A')
head(reviews_count)
##         date year month day   weekday
## 1 2010-03-14 2010     3  14    Sunday
## 2 2010-03-23 2010     3  23   Tuesday
## 3 2010-04-10 2010     4  10  Saturday
## 4 2010-04-21 2010     4  21 Wednesday
## 5 2010-04-26 2010     4  26    Monday
## 6 2010-05-10 2010     5  10    Monday

Se agrupa por año, mes y día de la semana para ver la evolución por cada tipo de variable.

year <- aggregate(reviews_count$date, by = list(reviews_count$year), FUN = length)
colnames(year) <- c("Year","number_of_reviews")
month <- aggregate(reviews_count$date, by = list(reviews_count$month), FUN = length)
colnames(month) <- c("Month","number_of_reviews")
weekday <- aggregate(reviews_count$date, by = list(reviews_count$weekday), FUN = length)
colnames(weekday) <- c("Weekday", "number_of_reviews")

ggplot(reviews_count, aes(x=year)) +
  geom_bar(col ='black', fill = 'blue')+
  scale_x_discrete(limits = c(2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021)) +
  labs (title ="Reseñas por año", x = "Año", y ="Número de alojamientos")
## Warning: Continuous limits supplied to discrete scale.
## Did you mean `limits = factor(...)` or `scale_*_continuous()`?

Como se aprecia en el gráfico, el negocio de los alojamientos turísticos crecía de forma exponencial desde el año 2015. La brusca caída se debe a los efectos del COVID-19 en el año 2020, situación que continua en 2021.

Nota: los últimos datos del dataset son de Abril de 2021, por lo que no se puede apreciar en el gráfico si hay recuperación con respecto a 2020 o no.

ggplot(reviews_count, aes(x=month)) +
  geom_bar(col ='black', fill = 'blue')+
  scale_x_discrete(limits = c("Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre")) +
  labs (title ="Reseñas por mes", x = "Mes", y ="Número de alojamientos")+
  coord_flip()

Sorprende que las reseñas se mantienen más o menos constantes a lo largo del año. Aún más que sean los meses de verano donde menos reseñas se escriben.

ggplot(data=reviews_count, aes(x=weekday)) + 
  geom_bar(col='black', fill ='blue') +
  labs (title = "Reseñas por días", x = "Días", y="Número de alojamientos")

Mientras que las reseñas permanecen constante a lo largo de los días de la semana, se ve un fuerte incremento en los domingos y lunes, lo que nos indica que los fines de semana es cuando más se reservan los alojamientos turísticos.

3.3.12 ¿Se podría construir un modelo de regresión para predecir el precio del alojamiento en función de otras variables?

3.3.12.1 Modelo de regresión lineal múltiple

Se busca predecir el precio de alquiler de un alojamiento turístico (variable dependiente cuantitativa) en función de una serie de variables cualitativas y cuantitativas (variables independientes). Para ello se utiliza un modelo de regresión lineal múltiple.

Se ha comprobado antes que la variable price no sigue una distribución normal, afectada sobre todo, por valores extremos debido al lujo. Se utiliza una transformación logarítmica para acercar más su distribución a la normalidad.

df_listings$price_trans <- log2(df_listings$price)
qqnorm(df_listings$price_trans); qqline(df_listings$price_trans)

Tras hacer la transformación, se observa que la nueva variable price_trans se acerca más a una distribución normal.

El modelo de regresión lineal emplea la relación lineal entre dos variables para predecir el valor que va a tomar una en función del valor que toma la otra. La condición para que funcione es que ambas variables estén correlacionadas. El primer paso a la hora de plantear un modelo de regresión lineal es hacer un estudio de correlación.

df_reg <- df_listings[,c("price_trans", "minimum_nights", "reviews_per_month", "availability_365", "latitude", "longitude")]

correlacionMatrix <- cor(df_reg)
corrplot(corr = cor(correlacionMatrix, method = "pearson"), method = "number", tl.cex = 0.7,number.cex = 0.8, cl.pos = "n")

# Generamos un dataset con las variables que van a formar parte del estudio
df_reg <- df_listings[,c("price_trans", "minimum_nights", "reviews_per_month", "availability_365", "latitude", "longitude","neighbourhood_group","room_type")]

Se observa que la correlación entre el precio y el resto de las variables es bajo.

La correlación más alta se da entre price_trans y longitude, pero es muy baja (-0.34).

Se convierten en factores los distintos valores de las variables categóricas, y se usan los valores con mayor peso como valores de referencia. En este caso serán el distrito Centro y el tipo de habitación Entire home/apartment.

df_reg$neighbourhood_group <- as.factor(df_reg$neighbourhood_group)
df_reg$room_type <- as.factor(df_reg$room_type)
df_reg$neighbourhood_group <- relevel(df_reg$neighbourhood_group, ref="Centro")
df_reg$room_type <- relevel (df_reg$room_type, ref="Entire home/apt")

Se crean distintos modelos usando distintas combinaciones de las variables cualitativas y cuantitativas

model1 <- lm(price_trans ~., data=df_reg)
model2 <- lm(price_trans ~ reviews_per_month + room_type + neighbourhood_group, data=df_reg)
model3 <- lm(price_trans ~ reviews_per_month + longitude + latitude + room_type + neighbourhood_group, data=df_reg)
model4 <- lm(price_trans ~ reviews_per_month + longitude + latitude + minimum_nights + room_type + neighbourhood_group, data=df_reg)
model5 <- lm(price_trans ~ minimum_nights + room_type + neighbourhood_group, data=df_reg)
model6 <- lm(price_trans ~ availability_365 + room_type + neighbourhood_group, data=df_reg)
model7 <- lm(price_trans ~ reviews_per_month + room_type + neighbourhood_group + longitude:latitude, data=df_reg)
model8 <- lm(price_trans ~ room_type + neighbourhood_group + reviews_per_month:availability_365, data=df_reg)
model9 <- lm(price_trans ~ room_type + neighbourhood_group, data=df_reg)

La bondad del ajuste se mide con el parámetro Adjusted R-squared, cuándo este valor es próximo a 1 es indicativo de un buen modelo de ajuste. Se utiliza este parámetro para elegir el modelo con el mejor coeficiente.

#Generamos los coeficientes de determinación de cada modelo
tabla.coef <- matrix(c(1, summary(model1)$r.squared,
                       2, summary(model2)$r.squared,
                       3, summary(model3)$r.squared,
                       4, summary(model4)$r.squared,
                       5, summary(model5)$r.squared,
                       6, summary(model6)$r.squared,
                       7, summary(model7)$r.squared,
                       8, summary(model8)$r.squared,
                       9, summary(model9)$r.squared),
                     ncol=2,byrow=TRUE)
colnames(tabla.coef) <-c("Modelo","R-squared")
tabla.coef
##       Modelo R-squared
##  [1,]      1 0.4512104
##  [2,]      2 0.4374237
##  [3,]      3 0.4385758
##  [4,]      4 0.4486447
##  [5,]      5 0.4264732
##  [6,]      6 0.4205993
##  [7,]      7 0.4374426
##  [8,]      8 0.4278817
##  [9,]      9 0.4193874

Se elige como mejor modelo a áquel que tiene el coeficiente de determinación mayor. En este caso el modelo 1.

Por último, para profundizar en la calidad del ajuste deben analizarse los residuos que nos indicarán realmente cómo se ajusta nuestro modelo a los datos muestrales.

residuos <- model1$residuals
summary(residuos)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
## -3.08542 -0.51001 -0.06726  0.00000  0.42470  3.66172
boxplot(residuos)

qqnorm(residuos)
qqline(residuos)

Los residuos siguen una distribución aparentemente normal, la distribución de los datos es simétrica respecto a su mediana, y el tamaño de la caja desde el primer al tercer cuartil es también simétrica respecto a la mediana, que está a su vez, centrada en el 0.

A continuación, se genera también la gráfica de residuos vs valores ajustados. Esta gráfica muestra los residuos en el eje Y y los valores ajustados en el eje X. Se utiliza para verificar el supuesto de que los residuos están distribuidos aleatoriamente y tienen una varianza constante. Lo ideal es que los puntos se ubiquen aleatoriamente a ambos lados del eje X.

Si existe una dispersión en abanico es síntoma de que la varianza no es constante. Esto también se aprecia en la línea azul que fluctúa a lo largo del eje X.

# Comparamos los valores ajustados con los residuos
ggplot(data=df_reg, aes(model1$fitted.values, model1$residuals)) + 
  geom_point() + 
  geom_smooth(color ="blue", se = FALSE)+
  geom_hline(yintercept = 0) + 
  ggtitle('Valores ajustados vs Residuos')+
  theme_bw()
## `geom_smooth()` using method = 'gam' and formula 'y ~ s(x, bs = "cs")'

Respecto a la bondad del modelo, se observan los siguientes parámetros:

summary(model1)
## 
## Call:
## lm(formula = price_trans ~ ., data = df_reg)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.0854 -0.5100 -0.0673  0.4247  3.6617 
## 
## Coefficients:
##                                            Estimate Std. Error  t value
## (Intercept)                              -1.609e+02  2.903e+01   -5.542
## minimum_nights                           -9.002e-03  4.721e-04  -19.070
## reviews_per_month                        -1.217e-01  4.528e-03  -26.887
## availability_365                          3.543e-04  3.836e-05    9.236
## latitude                                  4.205e+00  7.163e-01    5.871
## longitude                                 7.106e-01  6.212e-01    1.144
## neighbourhood_groupArganzuela            -1.619e-01  2.699e-02   -5.998
## neighbourhood_groupBarajas               -3.013e-01  1.025e-01   -2.940
## neighbourhood_groupCarabanchel           -2.854e-01  3.986e-02   -7.160
## neighbourhood_groupChamartín             -9.667e-02  4.399e-02   -2.197
## neighbourhood_groupChamberí              -6.278e-02  2.664e-02   -2.357
## neighbourhood_groupCiudad Lineal         -4.857e-01  4.900e-02   -9.912
## neighbourhood_groupFuencarral - El Pardo -5.765e-01  6.816e-02   -8.458
## neighbourhood_groupHortaleza             -4.383e-01  6.911e-02   -6.342
## neighbourhood_groupLatina                -3.005e-01  4.197e-02   -7.161
## neighbourhood_groupMoncloa - Aravaca     -8.170e-02  3.881e-02   -2.105
## neighbourhood_groupMoratalaz             -4.180e-01  8.215e-02   -5.088
## neighbourhood_groupPuente de Vallecas    -5.381e-01  4.491e-02  -11.983
## neighbourhood_groupRetiro                -3.933e-02  3.530e-02   -1.114
## neighbourhood_groupSalamanca              1.296e-01  2.976e-02    4.353
## neighbourhood_groupSan Blas - Canillejas -3.578e-01  7.449e-02   -4.803
## neighbourhood_groupTetuán                -3.204e-01  4.072e-02   -7.868
## neighbourhood_groupUsera                 -2.967e-01  5.271e-02   -5.629
## neighbourhood_groupVicálvaro             -5.011e-01  1.190e-01   -4.210
## neighbourhood_groupVilla de Vallecas     -4.452e-02  1.013e-01   -0.440
## neighbourhood_groupVillaverde            -2.912e-01  7.554e-02   -3.855
## room_typeHotel room                      -2.082e-01  6.466e-02   -3.219
## room_typePrivate room                    -1.201e+00  1.196e-02 -100.363
## room_typeShared room                     -1.707e+00  4.322e-02  -39.486
##                                          Pr(>|t|)    
## (Intercept)                              3.03e-08 ***
## minimum_nights                            < 2e-16 ***
## reviews_per_month                         < 2e-16 ***
## availability_365                          < 2e-16 ***
## latitude                                 4.41e-09 ***
## longitude                                0.252698    
## neighbourhood_groupArganzuela            2.04e-09 ***
## neighbourhood_groupBarajas               0.003282 ** 
## neighbourhood_groupCarabanchel           8.38e-13 ***
## neighbourhood_groupChamartín             0.028011 *  
## neighbourhood_groupChamberí              0.018452 *  
## neighbourhood_groupCiudad Lineal          < 2e-16 ***
## neighbourhood_groupFuencarral - El Pardo  < 2e-16 ***
## neighbourhood_groupHortaleza             2.32e-10 ***
## neighbourhood_groupLatina                8.31e-13 ***
## neighbourhood_groupMoncloa - Aravaca     0.035279 *  
## neighbourhood_groupMoratalaz             3.65e-07 ***
## neighbourhood_groupPuente de Vallecas     < 2e-16 ***
## neighbourhood_groupRetiro                0.265296    
## neighbourhood_groupSalamanca             1.35e-05 ***
## neighbourhood_groupSan Blas - Canillejas 1.58e-06 ***
## neighbourhood_groupTetuán                3.80e-15 ***
## neighbourhood_groupUsera                 1.84e-08 ***
## neighbourhood_groupVicálvaro             2.56e-05 ***
## neighbourhood_groupVilla de Vallecas     0.660200    
## neighbourhood_groupVillaverde            0.000116 ***
## room_typeHotel room                      0.001288 ** 
## room_typePrivate room                     < 2e-16 ***
## room_typeShared room                      < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.7387 on 18248 degrees of freedom
## Multiple R-squared:  0.4512, Adjusted R-squared:  0.4504 
## F-statistic: 535.8 on 28 and 18248 DF,  p-value: < 2.2e-16
  • Residual standard error: 0.7378 on 18248 degrees of freedom.

  • Multiple R-squared: 0.4512, Adjusted R-squared: 0.4504

  • F-statistic: 535.8 on 28 and 18248 DF, p-value: < 2.2e-16.

El RSE (Residual standard error) es la desviación estandar de los residuos, cuánto menor mejor.

El modelo es capaz de explicar el 45% de la variabilidad observada en los precios.

El test F muestra un p-value menor de 0.05 por lo que el modelo en conjunto es significativo.

Otra manera de comprobar la bondad del ajuste es realizar una validación cruzada. La validación cruzada es un método de remuestreo iterativo. Consiste en dividir los datos de forma aleatoria en k grupos del mismo tamaño, k-1 grupos se emplean para entrenar el modelo y uno de los grupos se emplea como validación. Este proceso se repite k veces un grupo distinto como validación en cada iteración. El proceso genera k estimaciones del error cuyo promedio se utiliza como estimación final.

En este caso aplicamos una validación cruzada (k-fold cross validation) con k= 10

set.seed(123)

train.control <- trainControl(method="cv", number=10)

model <- train(price_trans ~., data=df_reg, method="lm", trControl = train.control)

print(model)
## Linear Regression 
## 
## 18277 samples
##     7 predictor
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold) 
## Summary of sample sizes: 16448, 16448, 16450, 16451, 16450, 16449, ... 
## Resampling results:
## 
##   RMSE      Rsquared   MAE      
##   0.739209  0.4497992  0.5762033
## 
## Tuning parameter 'intercept' was held constant at a value of TRUE

Como se puede apreciar los resultados son prácticamente iguales a los anteriormente explicados.


4 Conclusiones


Atendiendo a las respuestas obtenidas en todas y cada una de las preguntas que se han realizado se llegan a las siguientes conclusiones:

- ¿Qué tipo de vivienda comprar?

Viviendas de segunda mano, de tamaño mediano-pequeño, a reformar.

El hecho de comprar viviendas de segundo mano se explica por la ausencia de promociones de obra nueva en las zonas de interés. Se explica en el siguiente punto cuáles son estas zonas.

Viviendas de tamaño mediano-pequeño, ya que la vivienda preferida por los usuarios de Airbnb son los apartamentos enteros. El precio se incrementa hasta un 250% con respecto a alquilar una habitación compartida. Para compensar esto, se requerirían varias habitaciones privadas en el mismo inmueble, pero implicaría inmuebles más caros de comprar y de reformar.

En el análisis de palabras más utilizadas en los anuncios se observan muchos términos relacionados con la localización y tipo de inmueble, pero también hay adjetivos como: stylish, acogedor, precioso, beautiful, cozy, luxury, lovely, luminosa, spacious, lo que indica una necesidad de transformar los inmuebles de acuerdo a las tendencias de decoración minimalista actuales.

- ¿En qué zona de la capital?

Viviendas en la zona de la almendra central pero fuera del Centro debido a la saturación del mismo (17 habitantes por alojamiento turístico).

Se recomiendan oportunidades en barrios más caros como Chamberí, Salamanca, y Retiro, y centrar los esfuerzos en adquirir imuebles en zonas de la almendra central con menor coste: Arganzuela, Tetuán, Chamartín. Otra opción muy interesante por precio y su proximidad al aeropuerto sería Barajas.

Las oportunidades serían ventas de inmuebles ya destinados al alquiler turístico en el que la inversión por reforma no sería necesaria. Sería posible encontrar estas oportunidades debido a los efectos de la pandemia.

- ¿Qué destino se le va a dar al inmueble?

La inversión debe destinarse al alquiler turístico. Es un mercado que hasta la pandemia de COVID-19 crecía de forma exponencial y al que se ven signos de recuperación.

Según las estadísticas del Ayuntamiento de Madrid la superficie media de la vivienda en Madrid es de 82 m2.

Según el portal idealista el precio medio del alquiler en Madrid en Noviembre de 2021 es de 14,6 euros/metro cuadrado. Por lo tanto, el precio medio del aquiler tradicional es de 1197 euros al mes.

En este ejercicio se ha comprobado que el precio medio del alquiler turístico es de 88 euros/noche, por lo que llegaría a los 2640 euros al mes (considerando 30 días). Un 220% más.

El análisis de estacionalidad del alquiler turístico también nos muestra homogeneidad a lo largo de los meses y una alta ocupación durante todos los días de la semana.


5 Contribución


Contribuciones Firma
Investigación previa BLB, GRF
Redacción de las respuestas BLB, GRF
Desarrollo del código BLB, GRF